﻿// Autore: Alessio Parma

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.Contracts;
using System.Linq;

namespace RecipesManager.Data.Operations
{
    sealed class RecipeClassifier
    {
        readonly IDictionary<IRecipe, IDictionary<IGuest, IDictionary<ICategory, Issue>>> _categIntIssues;
        readonly IDictionary<IRecipe, IDictionary<IGuest, IDictionary<ICategory, Issue>>> _categRatIssues;
        readonly IDictionary<IRecipe, HashSet<ICategory>> _categories = new Dictionary<IRecipe, HashSet<ICategory>>();
        readonly IDictionary<IRecipe, IDictionary<IGuest, IDictionary<IIngredient, Issue>>> _ingrIntIssues;
        readonly IDictionary<IRecipe, IDictionary<IGuest, IDictionary<IIngredient, Issue>>> _ingrRatIssues;

        readonly IDictionary<IRecipe, HashSet<IIngredient>> _ingredients =
            new Dictionary<IRecipe, HashSet<IIngredient>>();

        readonly IDictionary<IRecipe, int> _intollCount = new Dictionary<IRecipe, int>();

        readonly ObservableCollection<RatedRecipe> _ratedColl = new ObservableCollection<RatedRecipe>();
        readonly IDictionary<IRecipe, RatedRecipe> _ratedDict = new Dictionary<IRecipe, RatedRecipe>();
        readonly IDictionary<IRecipe, IDictionary<IGuest, Issue>> _recipeIssues;
        readonly IList<IRecipe> _recipes;
        readonly IDictionary<IRecipe, int> _scores = new Dictionary<IRecipe, int>();
        readonly Classifier _classifier;
        bool _collHasChanged;

        public RecipeClassifier(IList<IGuest> guests, Classifier classifier)
        {
            _classifier = classifier;

            _recipeIssues = new Dictionary<IRecipe, IDictionary<IGuest, Issue>>();
            _ingrRatIssues = new Dictionary<IRecipe, IDictionary<IGuest, IDictionary<IIngredient, Issue>>>();
            _ingrIntIssues = new Dictionary<IRecipe, IDictionary<IGuest, IDictionary<IIngredient, Issue>>>();
            _categRatIssues = new Dictionary<IRecipe, IDictionary<IGuest, IDictionary<ICategory, Issue>>>();
            _categIntIssues = new Dictionary<IRecipe, IDictionary<IGuest, IDictionary<ICategory, Issue>>>();

            var d1 = guests.ToDictionary<IGuest, IGuest, Issue>(og => og, og => null);
            var d2 = guests.ToDictionary<IGuest, IGuest, IDictionary<IIngredient, Issue>>(og => og, og => null);
            var d3 = guests.ToDictionary<IGuest, IGuest, IDictionary<ICategory, Issue>>(og => og, og => null);

            _recipes = Manager.Recipes.Unordered.ToList();
            foreach (var r in _recipes)
            {
                _scores.Add(r, 0);
                _intollCount.Add(r, 0);

                _recipeIssues.Add(r, new Dictionary<IGuest, Issue>(d1));
                _ingrRatIssues.Add(r, new Dictionary<IGuest, IDictionary<IIngredient, Issue>>(d2));
                _ingrIntIssues.Add(r, new Dictionary<IGuest, IDictionary<IIngredient, Issue>>(d2));
                _categRatIssues.Add(r, new Dictionary<IGuest, IDictionary<ICategory, Issue>>(d3));
                _categIntIssues.Add(r, new Dictionary<IGuest, IDictionary<ICategory, Issue>>(d3));

                var rr = new RatedRecipe(r);
                _ratedDict.Add(r, rr);
                _ratedColl.Add(rr);

                AddRecipeIngredients(r);
                AddRecipeCategories(r);
            }
        }

        public ObservableCollection<RatedRecipe> RatedRecipes
        {
            get { return _ratedColl; }
        }

        public void OnGuestAdd(IGuest guest)
        {
            var edited = new HashSet<IRecipe>();

            foreach (var rr in guest.RecipeRatings)
            {
                var recipe = rr.Entity;
                CheckRecipeRatingOnAdd(guest, recipe, rr.Rating);
                _scores[recipe] += Utils.RecipeRatingToInt(rr.Rating);
                edited.Add(recipe);
            }

            foreach (var ir in guest.IngredientRatings)
            {
                foreach (var r in RecipesWithIngredient(ir.Entity))
                {
                    CheckIngredientRatingOnAdd(guest, ir.Entity, r, ir.Rating);
                    _scores[r] += Utils.IngredientRatingToInt(ir.Rating);
                    edited.Add(r);
                }
            }

            foreach (var cr in guest.CategoryRatings)
            {
                foreach (var r in RecipesWithCategory(cr.Entity))
                {
                    CheckCategoryRatingOnAdd(guest, cr.Entity, r, cr.Rating);
                    _scores[r] += Utils.CategoryRatingToInt(cr.Rating);
                    edited.Add(r);
                }
            }

            foreach (var i in guest.IngredientIntolerances)
            {
                foreach (var r in RecipesWithIngredient(i))
                {
                    CheckIngredientIntoleranceOnAdd(guest, i, r);
                    _intollCount[r]++;
                    edited.Add(r);
                }
            }

            foreach (var c in guest.CategoryIntolerances)
            {
                foreach (var r in RecipesWithCategory(c))
                {
                    CheckCategoryIntoleranceOnAdd(guest, c, r);
                    _intollCount[r]++;
                    edited.Add(r);
                }
            }

            foreach (var e in edited)
            {
                var rr = _ratedDict[e];
                if (_intollCount[e] > 0)
                {
                    UpdateType(rr, RecipeType.Dangerous);
                    continue;
                }
                UpdateType(rr, Utils.RecipeTypeForScore(_scores[e]));
            }
            UpdateCollection();
        }

        public void OnGuestRemove(IGuest guest)
        {
            var edited = new HashSet<IRecipe>();

            foreach (var rr in guest.RecipeRatings)
            {
                var recipe = rr.Entity;
                CheckRecipeRatingOnRemove(guest, recipe, rr.Rating);
                _scores[recipe] -= Utils.RecipeRatingToInt(rr.Rating);
                edited.Add(recipe);
            }

            foreach (var ir in guest.IngredientRatings)
            {
                foreach (var r in RecipesWithIngredient(ir.Entity))
                {
                    CheckIngredientRatingOnRemove(guest, ir.Entity, r, ir.Rating);
                    _scores[r] -= Utils.IngredientRatingToInt(ir.Rating);
                    edited.Add(r);
                }
            }

            foreach (var cr in guest.CategoryRatings)
            {
                foreach (var r in RecipesWithCategory(cr.Entity))
                {
                    CheckCategoryRatingOnRemove(guest, cr.Entity, r, cr.Rating);
                    _scores[r] -= Utils.CategoryRatingToInt(cr.Rating);
                    edited.Add(r);
                }
            }

            foreach (var i in guest.IngredientIntolerances)
            {
                foreach (var r in RecipesWithIngredient(i))
                {
                    CheckIngredientIntoleranceOnRemove(guest, i, r);
                    _intollCount[r]--;
                    edited.Add(r);
                }
            }

            foreach (var c in guest.CategoryIntolerances)
            {
                foreach (var r in RecipesWithCategory(c))
                {
                    CheckCategoryIntoleranceOnRemove(guest, c, r);
                    _intollCount[r]--;
                    edited.Add(r);
                }
            }

            foreach (var e in edited)
            {
                var rr = _ratedDict[e];
                if (_intollCount[e] > 0)
                {
                    UpdateType(rr, RecipeType.Dangerous);
                    continue;
                }
                UpdateType(rr, Utils.RecipeTypeForScore(_scores[e]));
            }
            UpdateCollection();
        }

        void AddRecipeIngredients(IRecipe recipe)
        {
            if (_ingredients.ContainsKey(recipe)) return;
            var set = new HashSet<IIngredient>(recipe.Ingredients.Select(i => i.Ingredient));
            foreach (var sr in recipe.Steps.Where(sr => sr.Type == StepType.SubRecipe))
            {
                AddRecipeIngredients(sr.SubRecipe);
                foreach (var i in _ingredients[sr.SubRecipe])
                    set.Add(i);
            }
            _ingredients.Add(recipe, set);
        }

        void AddRecipeCategories(IRecipe recipe)
        {
            var categories = _ingredients[recipe].SelectMany(IngredientCategories);
            _categories.Add(recipe, new HashSet<ICategory>(categories));
        }

        static IEnumerable<ICategory> IngredientCategories(IIngredient ingredient)
        {
            var stack = new Stack<ICategory>(ingredient.Categories);
            while (stack.Count > 0)
            {
                var c = stack.Pop();
                yield return c;
                foreach (var p in c.Parents) stack.Push(p);
            }
        }

        public void OnRecipeAdd(IRecipe recipe)
        {
            _ratedColl.Remove(_ratedDict[recipe]);

            foreach (var gr in recipe.GuestRatings)
                CheckRecipeRatingOnAdd(gr.Entity, recipe, gr.Rating);

            foreach (var i in _ingredients[recipe])
            {
                foreach (var g in i.GuestRatings)
                    CheckIngredientRatingOnAdd(g.Entity, i, recipe, g.Rating);
                foreach (var g in i.GuestIntolerances)
                    CheckIngredientIntoleranceOnAdd(g, i, recipe);
            }

            foreach (var c in _categories[recipe])
            {
                foreach (var g in c.GuestRatings)
                    CheckCategoryRatingOnAdd(g.Entity, c, recipe, g.Rating);
                foreach (var g in c.GuestIntolerances)
                    CheckCategoryIntoleranceOnAdd(g, c, recipe);
            }
        }

        public void OnRecipeRemove(IRecipe recipe)
        {
            foreach (var gr in recipe.GuestRatings)
                CheckRecipeRatingOnRemove(gr.Entity, recipe, gr.Rating);

            foreach (var i in _ingredients[recipe])
            {
                foreach (var g in i.GuestRatings)
                    CheckIngredientRatingOnRemove(g.Entity, i, recipe, g.Rating);
                foreach (var g in i.GuestIntolerances)
                    CheckIngredientIntoleranceOnRemove(g, i, recipe);
            }

            foreach (var c in _categories[recipe])
            {
                foreach (var g in c.GuestRatings)
                    CheckCategoryRatingOnRemove(g.Entity, c, recipe, g.Rating);
                foreach (var g in c.GuestIntolerances)
                    CheckCategoryIntoleranceOnRemove(g, c, recipe);
            }

            _ratedColl.Add(_ratedDict[recipe]);
        }

        void CheckRecipeRatingOnAdd(IGuest guest, IRecipe recipe, FoodRating rating)
        {
            if (rating != FoodRating.Bad && rating != FoodRating.VeryBad) return;
            if (!_classifier.Contains(recipe) || !_classifier.Contains(guest)) return;

            var issue = Issue.ForRecipeRating(guest, recipe, rating);
            if (_recipeIssues[recipe][guest] != null) return;
            _recipeIssues[recipe][guest] = issue;
            _classifier.Issues.Add(issue);
        }

        void CheckRecipeRatingOnRemove(IGuest guest, IRecipe recipe, FoodRating rating)
        {
            if (rating != FoodRating.Bad && rating != FoodRating.VeryBad) return;
            if (!_classifier.Contains(recipe) || !_classifier.Contains(guest)) return;

            var issue = _recipeIssues[recipe][guest];
            if (issue == null) return;
            _classifier.Issues.Remove(issue);
            _recipeIssues[recipe][guest] = null;
        }

        void CheckIngredientRatingOnAdd(IGuest guest, IIngredient ingredient, IRecipe recipe, FoodRating rating)
        {
            CheckFoodRatingOnAdd(_ingrRatIssues, Issue.ForIngredientRating, guest, ingredient, recipe, rating);
        }

        void CheckIngredientRatingOnRemove(IGuest guest, IIngredient ingredient, IRecipe recipe, FoodRating rating)
        {
            CheckFoodRatingOnRemove(_ingrRatIssues, guest, ingredient, recipe, rating);
        }

        void CheckIngredientIntoleranceOnAdd(IGuest guest, IIngredient ingredient, IRecipe recipe)
        {
            CheckFoodIntoleranceOnAdd(_ingrIntIssues, Issue.ForIngredientIntolerance, guest, ingredient, recipe);
        }

        void CheckIngredientIntoleranceOnRemove(IGuest guest, IIngredient ingredient, IRecipe recipe)
        {
            CheckFoodIntoleranceOnRemove(_ingrIntIssues, guest, ingredient, recipe);
        }

        void CheckCategoryRatingOnAdd(IGuest guest, ICategory category, IRecipe recipe, FoodRating rating)
        {
            CheckFoodRatingOnAdd(_categRatIssues, Issue.ForCategoryRating, guest, category, recipe, rating);
        }

        void CheckCategoryRatingOnRemove(IGuest guest, ICategory category, IRecipe recipe, FoodRating rating)
        {
            CheckFoodRatingOnRemove(_categRatIssues, guest, category, recipe, rating);
        }

        void CheckCategoryIntoleranceOnAdd(IGuest guest, ICategory category, IRecipe recipe)
        {
            CheckFoodIntoleranceOnAdd(_categIntIssues, Issue.ForCategoryIntolerance, guest, category, recipe);
        }

        void CheckCategoryIntoleranceOnRemove(IGuest guest, ICategory category, IRecipe recipe)
        {
            CheckFoodIntoleranceOnRemove(_categIntIssues, guest, category, recipe);
        }

        void CheckFoodRatingOnAdd<T>(IDictionary<IRecipe, IDictionary<IGuest, IDictionary<T, Issue>>> dict,
                                     Func<IGuest, T, IRecipe, FoodRating, Issue> issue, IGuest guest, T food,
                                     IRecipe recipe, FoodRating rating)
        {
            if (rating != FoodRating.Bad && rating != FoodRating.VeryBad) return;
            if (!_classifier.Contains(recipe) || !_classifier.Contains(guest)) return;

            var foodIssues = dict[recipe][guest];
            if (foodIssues == null)
            {
                foodIssues = new Dictionary<T, Issue>();
                dict[recipe][guest] = foodIssues;
            }
            if (foodIssues.ContainsKey(food)) return;
            var i = issue(guest, food, recipe, rating);
            foodIssues.Add(food, i);
            _classifier.Issues.Add(i);
        }

        void CheckFoodRatingOnRemove<T>(IDictionary<IRecipe, IDictionary<IGuest, IDictionary<T, Issue>>> dict,
                                        IGuest guest, T food, IRecipe recipe, FoodRating rating)
        {
            if (rating != FoodRating.Bad && rating != FoodRating.VeryBad) return;
            if (!_classifier.Contains(recipe) || !_classifier.Contains(guest)) return;

            var foodIssues = dict[recipe][guest];
            Issue issue;
            if (foodIssues == null || !foodIssues.TryGetValue(food, out issue)) return;
            _classifier.Issues.Remove(issue);
            foodIssues.Remove(food);
        }

        void CheckFoodIntoleranceOnAdd<T>(IDictionary<IRecipe, IDictionary<IGuest, IDictionary<T, Issue>>> dict,
                                          Func<IGuest, T, IRecipe, Issue> issue, IGuest guest, T food, IRecipe recipe)
        {
            if (!_classifier.Contains(recipe) || !_classifier.Contains(guest)) return;

            var foodIssues = dict[recipe][guest];
            if (foodIssues == null)
            {
                foodIssues = new Dictionary<T, Issue>();
                dict[recipe][guest] = foodIssues;
            }
            if (foodIssues.ContainsKey(food)) return;
            var i = issue(guest, food, recipe);
            foodIssues.Add(food, i);
            _classifier.Issues.Add(i);
        }

        void CheckFoodIntoleranceOnRemove<T>(IDictionary<IRecipe, IDictionary<IGuest, IDictionary<T, Issue>>> dict,
                                             IGuest guest, T food, IRecipe recipe)
        {
            if (!_classifier.Contains(recipe) || !_classifier.Contains(guest)) return;

            var foodIssues = dict[recipe][guest];
            Issue issue;
            if (foodIssues == null || !foodIssues.TryGetValue(food, out issue)) return;
            _classifier.Issues.Remove(issue);
            foodIssues.Remove(food);
        }

        IEnumerable<IRecipe> RecipesWithIngredient(IIngredient ingredient)
        {
            return _recipes.Where(r => _ingredients[r].Contains(ingredient));
        }

        IEnumerable<IRecipe> RecipesWithCategory(ICategory category)
        {
            return _recipes.Where(r => _categories[r].Contains(category));
        }

        void UpdateType(RatedRecipe rr, RecipeType type)
        {
            var score = _scores[rr.Recipe];
            rr.Rating = (type == RecipeType.Dangerous) ? int.MinValue : score;
            rr.Type = type;
            _collHasChanged = true;
        }

        void UpdateCollection()
        {
            if (_collHasChanged && RatedRecipes.Count > 0)
                RatedRecipes.First().UpdateCollection();
            _collHasChanged = false;
        }

        [ContractInvariantMethod]
        void Invariant()
        {
            Contract.Invariant(RatedRecipes != null);
        }
    }
}